<?php declare(strict_types=1);
/**
 * @author      xianganyall <xianganyall@gmail.com>
 * @copyright   2023-2025 owner
 **/

namespace Srv\Libs\Plugins\MapGeo;

class MapGeo
{
    public const EARTH_RADIUS       = 6370996.81;       // 地球半径
    private ?Polygon $chinaPolygon  = null;

    /**
     * @return Polygon
     * 获取大致中国区域多边形对象
     */
    public function getChinaPolygon():Polygon
    {
        if($this->chinaPolygon instanceof Polygon) return $this->chinaPolygon;
        $pointString    = '124.78243:40.015793,126.769338:41.691995,127.946765:41.360177,128.314711:41.9672,128.682657:42.241212,129.933673:42.839837,130.669565:42.568451,130.890333:43.056084,131.479046:43.37902,131.552636:44.758491,131.994171:45.384248,132.656473:45.020061,135.011327:48.311666,132.435706:48.016269,131.18469:47.917424,128.756246:49.715347,127.50523:49.905839,125.518322:53.206869,120.955793:53.295142,120.219901:52.223742,118.821706:49.858287,116.61403:49.619818,115.73096:48.016269,119.925544:46.968884,106.67949:41.9672,96.597772:42.89397,87.987837:48.994276,80.334561:44.915577,80.334561:42.022098,73.785123:39.732278,79.009956:31.514752,88.355783:27.656501,97.039307:28.114234,98.952625:27.26261,97.821975:24.075389,101.648613:21.413571,101.722202:22.580287,106.579089:22.921571,109.743424:15.660271,108.418818:3.486313,113.570061:3.412359,123.063067:23.737094,123.578191:32.530562,123.357423:38.308766';
        $points         = [];
        foreach(explode(',', $pointString) as $val){
            $tmp        = explode(':', $val);
            $points[]   = new Point(floatval($tmp[0]), floatval($tmp[1]));
        }
        $this->chinaPolygon = new Polygon($points);
        return $this->chinaPolygon;
    }

    /**
     * @param Point $point
     * @param Bounds $bounds
     * @return bool
     * 判断点是否在矩形内
     */
    public function isPointInRect(Point $point, Bounds $bounds):bool
    {
        $sw     = $bounds->getSouthWest(); // 西南脚点
        $ne     = $bounds->getNorthEast(); // 东北脚点
        return ($point->lng >= $sw->lng && $point->lng <= $ne->lng && $point->lat >= $sw->lat && $point->lat <= $ne->lat);
    }

    /**
     * @param Point $point
     * @param Polygon $polygon
     * @return bool
     * 判断点是否多边形内
     */
    public function isPointInPolygon(Point $point, Polygon $polygon):bool
    {
        // 首先判断点是否在多边形的外包矩形内，如果在，则进一步判断，否则返回false
        if(!$this->isPointInRect($point, $polygon->getBounds())){
            return false;
        }
        $pts            = $polygon->getPath(); // 获取多边形点 getPath()
        // 在多边形内。还会考虑一些特殊情况，如点在多边形顶点上，点在多边形边上等特殊情况。
        $N              = count($pts);
        $intersectCount = 0;        // cross points count of x
        $precision      = 2e-10;    // 浮点类型计算时候与0比较时候的容差
        $p              = $point;
        $p1             = $pts[0];
        for($i = 1; $i <= $N; ++$i){
            if($p->equals($p1)) return true;
            $p2         = $pts[$i % $N];
            if($p->lat < min($p1->lat, $p2->lat) || $p->lat > max($p1->lat, $p2->lat)){
                $p1     = $p2;
                continue;
            }
            if($p->lat > min($p1->lat, $p2->lat) && $p->lat < max($p1->lat, $p2->lat)){
                if($p->lng <= max($p1->lng, $p2->lng)){
                    if($p1->lat == $p2->lat && $p->lng >= min($p1->lng, $p2->lng)) return true;
                    if($p1->lng == $p2->lng){
                        if($p1->lng == $p->lng){
                            return true;
                        }else{
                            ++$intersectCount;
                        }
                    }else{
                        $xinters = ($p->lat - $p1->lat) * ($p2->lng - $p1->lng) / ($p2->lat - $p1->lat) + $p1->lng;
                        if(abs($p->lng - $xinters) < $precision) return true;
                        if($p->lng < $xinters) ++$intersectCount;
                    }
                }
            }else{
                if($p->lat == $p2->lat && $p->lng <= $p2->lng){
                    $p3     = $pts[($i+1) % $N];
                    if($p->lat >= min($p1->lat, $p3->lat) && $p->lat <= max($p1->lat, $p3->lat)){
                        ++$intersectCount;
                    }else{
                        $intersectCount += 2;
                    }
                }
            }
            $p1         = $p2;
        }
        return !(($intersectCount % 2 == 0));
    }

    /**
     * @param float $degree
     * @return float
     * 将度转化为弧度
     */
    private function degreeToRad(float $degree):float
    {
        return M_PI * $degree/180;
    }

    /**
     * @param float $rad
     * @return float
     * 将弧度转化为度
     */
    private function radToDegree(float $rad):float
    {
        return (180 * $rad) / M_PI;
    }

    /**
     * @param float $v
     * @param float $a
     * @param float $b
     * @return float
     * 将v值限定在a,b之间，经度使用
     */
    private function _getLoop(float $v,float $a,float $b):float
    {
        while($v > $b) $v -= $b - $a;
        while($v < $a) $v += $b - $a;
        return $v;
    }

    /**
     * @param Point $point1
     * @param Point $point2
     * @return float
     * 计算两点之间的距离,单位为米
     */
    public function getDistance(Point $point1, Point $point2):float
    {
        $point1->lng = $this->_getLoop($point1->lng, -180.0, 180.0);
        $point1->lat = min(max($point1->lat, -74.0), 74.0);
        $point2->lng = $this->_getLoop($point2->lng, -180.0, 180.0);
        $point2->lat = min(max($point2->lat, -74.0), 74.0);
        $x1 = $this->degreeToRad($point1->lng);
        $y1 = $this->degreeToRad($point1->lat);
        $x2 = $this->degreeToRad($point2->lng);
        $y2 = $this->degreeToRad($point2->lat);
        return self::EARTH_RADIUS * acos((sin($y1) * sin($y2) + cos($y1) * cos($y2) * cos($x2 - $x1)));
    }

    /**
     * @param array $polyline
     * @return float
     * 计算折线或者点数组的长度
     */
    public function getPolylineDistance(array $polyline):float
    {
        $pts        = $polyline;
        $ptsLength  = count($pts);
        if($ptsLength < 2) return 0.0;  // 小于2个点，返回0
        // 遍历所有线段将其相加，计算整条线段的长度
        $totalDis   = 0;
        for($i = 0; $i < $ptsLength - 1; $i++){
            $curPt      = $pts[$i];
            $nextPt     = $pts[$i + 1];
            $dis        = $this->getDistance($curPt, $nextPt);
            $totalDis   += $dis;
        }
        return $totalDis;
    }

    /**
     * @param array $polygon
     * @return float
     * 计算多边形面或点数组构建图形的面积,不适合计算自相交多边形的面积
     */
    public function getPolygonArea(array $polygon):float
    {
        $pts        = $polygon;
        $ptsLength  = count($pts);
        if($ptsLength < 3) return 0.0;  // 小于3个顶点，不能构建面
        $Sum1       = 0.0;
        $Sum2       = 0.0;
        $Count2     = 0;
        $Count1     = 0;
        $Radius = self::EARTH_RADIUS; // 6378137.0,WGS84椭球半径
        $Count  = $ptsLength;
        for ($i = 0; $i < $Count; $i++){
            if ($i == 0){
                $LowX       = $pts[$Count - 1]->lng * M_PI / 180;
                $LowY       = $pts[$Count - 1]->lat * M_PI / 180;
                $MiddleX    = $pts[0]->lng * M_PI / 180;
                $MiddleY    = $pts[0]->lat * M_PI / 180;
                $HighX      = $pts[1]->lng * M_PI / 180;
                $HighY      = $pts[1]->lat * M_PI / 180;
            }else if($i == $Count - 1){
                $LowX       = $pts[$Count - 2]->lng * M_PI / 180;
                $LowY       = $pts[$Count - 2]->lat * M_PI / 180;
                $MiddleX    = $pts[$Count - 1]->lng * M_PI / 180;
                $MiddleY    = $pts[$Count - 1]->lat * M_PI / 180;
                $HighX      = $pts[0]->lng * M_PI / 180;
                $HighY      = $pts[0]->lat * M_PI / 180;
            }else{
                $LowX       = $pts[$i - 1]->lng * M_PI / 180;
                $LowY       = $pts[$i - 1]->lat * M_PI / 180;
                $MiddleX    = $pts[$i]->lng * M_PI / 180;
                $MiddleY    = $pts[$i]->lat * M_PI / 180;
                $HighX      = $pts[$i + 1]->lng * M_PI / 180;
                $HighY      = $pts[$i + 1]->lat * M_PI / 180;
            }
            $AM = cos($MiddleY) * cos($MiddleX);
            $BM = cos($MiddleY) * sin($MiddleX);
            $CM = sin($MiddleY);
            $AL = cos($LowY) * cos($LowX);
            $BL = cos($LowY) * sin($LowX);
            $CL = sin($LowY);
            $AH = cos($HighY) * cos($HighX);
            $BH = cos($HighY) * sin($HighX);
            $CH = sin($HighY);
            $CoefficientL   = ($AM * $AM + $BM * $BM + $CM * $CM) / ($AM * $AL + $BM * $BL + $CM * $CL);
            $CoefficientH   = ($AM * $AM + $BM * $BM + $CM * $CM) / ($AM * $AH + $BM * $BH + $CM * $CH);
            $ALtangent      = $CoefficientL * $AL - $AM;
            $BLtangent      = $CoefficientL * $BL - $BM;
            $CLtangent      = $CoefficientL * $CL - $CM;
            $AHtangent      = $CoefficientH * $AH - $AM;
            $BHtangent      = $CoefficientH * $BH - $BM;
            $CHtangent      = $CoefficientH * $CH - $CM;
            $AngleCos       = ($AHtangent * $ALtangent + $BHtangent * $BLtangent + $CHtangent * $CLtangent) / (sqrt($AHtangent * $AHtangent + $BHtangent * $BHtangent + $CHtangent * $CHtangent) * sqrt($ALtangent * $ALtangent + $BLtangent * $BLtangent + $CLtangent * $CLtangent));
            $AngleCos       = acos($AngleCos);
            $ANormalLine    = $BHtangent * $CLtangent - $CHtangent * $BLtangent;
            $BNormalLine    = 0 - ($AHtangent * $CLtangent - $CHtangent * $ALtangent);
            $CNormalLine    = $AHtangent * $BLtangent - $BHtangent * $ALtangent;
            if($AM != 0){
                $OrientationValue = $ANormalLine / $AM;
            }else if ($BM != 0){
                $OrientationValue = $BNormalLine / $BM;
            }else{
                $OrientationValue = $CNormalLine / $CM;
            }
            if($OrientationValue > 0){
                $Sum1 += $AngleCos;
                $Count1++;
            }else{
                $Sum2 += $AngleCos;
                $Count2++;
            }
        }
        $tempSum1   = $Sum1 + (2 * M_PI * $Count2 - $Sum2);
        $tempSum2   = (2 * M_PI * $Count1 - $Sum1) + $Sum2;
        if ($Sum1 > $Sum2) {
            if (($tempSum1 - ($Count - 2) * M_PI) < 1){
                $Sum = $tempSum1;
            }else{
                $Sum = $tempSum2;
            }
        }else{
            if (($tempSum2 - ($Count - 2) * M_PI) < 1){
                $Sum = $tempSum2;
            }else{
                $Sum = $tempSum1;
            }
        }
        return ($Sum - ($Count - 2) * M_PI) * $Radius * $Radius;
    }
}